﻿namespace XamlUidValidate
{
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Xml;
    using System.Xml.Linq;

    /// <summary>
    /// Program for checking that x:Uid declarations in a Xaml project has corresponding resources
    /// </summary>
    public class Program
    {
        private const string Projns = "http://schemas.microsoft.com/developer/msbuild/2003";
        private const string Xns = "http://schemas.microsoft.com/winfx/2006/xaml";

        /// <summary>
        /// Validates a project files. Writes warnings and errors to console
        /// </summary>
        /// <param name="args">Arguments to the program. First argument should be the full path of the project file to validate</param>
        public static void Main(string[] args)
        {
            Console.WriteLine("Starting analysing Uid declarations in {0}", args[0]);

            // Read the project file as xml
            string projectDirectory = Path.GetDirectoryName(args[0]);
            XDocument projectXml = XDocument.Load(args[0]);

            // Read all resource keys
            var resourceFiles = ReadResourceFiles(projectDirectory, projectXml).ToList();

            // Get all uid declarations on all pages
            IEnumerable<XElement> pageNodes = projectXml.Descendants(XName.Get("Page", Projns));
            var uids = pageNodes.SelectMany(node => ParsePage(projectDirectory, node)).ToList();

            // Verify we have resources
            if (uids.Any() && !resourceFiles.Any())
            {
                Console.WriteLine("{0} : Error : No string resources defined!", args[0]);
            }

            // Verify all resources are used
            resourceFiles.ToList().ForEach(file =>
                file.Keys.Where(key => uids.All(uid => uid.Uid != key)).ToList()
                    .ForEach(key => Console.WriteLine("{0} : Warning: Unused resource: \"{1}", file.File, key)));

            // Verify all uids has a resource key in all resource files
            var missingKeys = uids.Where(uid => resourceFiles.Any(file => file.Keys.All(key => key != uid.Uid)));
            missingKeys.ToList().ForEach(uid =>
                Console.WriteLine("{0} ({1},{2}) : Error : Resource missing for Uid: \"{3}\"",
                    uid.Filepath, uid.Line, uid.Position, uid.Uid));

            Console.WriteLine("Done analysing Uid declarations in {0}", args[0]);
        }

        /// <summary>
        /// Read all resource keys defined in a project
        /// </summary>
        /// <param name="projectDirectory">Path to directory of the project</param>
        /// <param name="projectXml">The project file as xml</param>
        /// <returns>Dictionary of resource keys per language</returns>
        private static IEnumerable<ResourceFile> ReadResourceFiles(string projectDirectory, XDocument projectXml)
        {
            IEnumerable<XElement> resourceNodes = projectXml.Descendants(XName.Get("PRIResource", Projns));
            IEnumerable<string> resourceFilenames =
                resourceNodes.Select(node => Path.Combine(projectDirectory, node.Attribute(XName.Get("Include")).Value));
            return resourceFilenames.Select(file => new ResourceFile
            {
                Language = new DirectoryInfo(Path.GetDirectoryName(file)).Name,
                File = file,
                Keys = ReadResourceKeys(file).ToList()
            });
        }

        /// <summary>
        /// Read all resource keys in a resource file
        /// Only keys containing a '.' is read and only the part before the '.' is returned
        /// </summary>
        /// <param name="resourceFile">Path to the file</param>
        /// <returns>A list of keys in the file</returns>
        private static IEnumerable<string> ReadResourceKeys(string resourceFile)
        {
            XDocument resourceXml = XDocument.Load(resourceFile);
            return resourceXml.Descendants(XName.Get("data"))
                .Select(data => data.Attribute(XName.Get("name")).Value)
                .Where(key => key.Contains("."))
                .Select(key => key.Split('.')[0])
                .Distinct();
        }

        /// <summary>
        /// Read a xaml page
        /// </summary>
        /// <param name="projectDirectory">Path to directory of the project</param>
        /// <param name="pageNode">The xaml page node in the project file</param>
        /// <returns></returns>
        private static IEnumerable<UidReference> ParsePage(string projectDirectory, XElement pageNode)
        {
            // Load the page xaml file
            string filepath = Path.Combine(projectDirectory, pageNode.Attribute(XName.Get("Include")).Value);
            XDocument pageXml = XDocument.Load(filepath, LoadOptions.SetLineInfo);

            // Get uids from nodes having the x:Uid attribute
            IEnumerable<XElement> uidNodes =
                pageXml.Descendants().Where(node => node.Attributes(XName.Get("Uid", Xns)).Any());
            var uids = uidNodes.Select(node => new UidReference
            {
                Uid = node.Attribute(XName.Get("Uid", Xns)).Value,
                Filepath = filepath,
                Line = ((IXmlLineInfo) node).LineNumber,
                Position = ((IXmlLineInfo) node).LinePosition
            }).ToList();
            CheckUniqueUid(uids);
            return uids;
        }

        /// <summary>
        /// Check if there are any duplicate uids and write a waring if there is
        /// </summary>
        /// <param name="uids">Enumeration of uids</param>
        private static void CheckUniqueUid(IEnumerable<UidReference> uids)
        {
            var duplicates = uids.GroupBy(uid => uid.Uid).Where(g => g.Count() > 1);
            duplicates.ToList().ForEach(group => group.ToList().ForEach(uid =>
                Console.WriteLine("{0} ({1},{2}) : Warning : Uid declared multiple times: \"{3}\"",
                    uid.Filepath, uid.Line, uid.Position, uid.Uid)));
        }
    }
}